#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         MultipleChoice.py
#
# Copyright:    Copyright (c) 2004, Faulkner Technologies
#
# Author:       Tom Cameron, Brian Oliver
#
# Description:  A MultipleChoice is a primitive Component
#               to represent simple multiple choice questions of 
#               mutually exclusive answers.
#
# See Also:     Component, Model
#####################################################################

#####################################################################
## Knowledge Engine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Component import Component, ComponentInfo
from Products.KnowledgeEngine.Assessible import Assessible
from Products.KnowledgeEngine.AssessmentFilter import AssessmentFilter
from Products.KnowledgeEngine.AssessmentComponentState import AssessmentComponentState
from Products.KnowledgeEngine.AssessmentComponentValueTypes import *
from Products.KnowledgeEngine.Exportable import Exportable
from Products.KnowledgeEngine.KERLExtensions import KERLExtensions
from Products.KnowledgeEngine.KERLProcessor import *
from Products.KnowledgeEngine.Scorable import *
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.KERFProcessor import *
from Products.KnowledgeEngine.KERLProcessor import InvalidKerlOperator
from Products.KnowledgeEngine.Graphing.Graphable import *
from Products.KnowledgeEngine.Graphing.Datum import *
from Products.KnowledgeEngine.User import User 
from Products.KnowledgeEngine.Caching import RequestCache 

#####################################################################
## Zope CMF Library Imports
from Products.CMFCore.PortalContent import PortalContent
from Products.CMFCore.ActionInformation import ActionInformation
from Products.CMFCore import permissions
from Products.CMFCore.permissions import ModifyPortalContent

#####################################################################
## Zope Library Imports
from Globals import DTMLFile
from App.ImageFile import ImageFile
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.Role import RoleManager

#####################################################################
## Python Library Imports
import string
import re
import types

#####################################################################
## Answer Class Definition
class Answer(RoleManager):
    """
        Answer instances are used to represent individual answers
        for the MultipleChoice component
    """

    #---------------------------------------------------------------#

    #define security
    security = ClassSecurityInfo()
    security.declareObjectPublic()
    security.setDefaultAccess("allow")

    #---------------------------------------------------------------#

    def __init__(self, answerID="A", content="", assessmentContent="", reportContent="", score=0, isCorrectValue=false, option="A"):
        """
            The constructor for the Answer
        """
        self.answerID = answerID
        self.content = content
        self.assessmentContent = assessmentContent
        self.reportContent = reportContent
        self.score = score
        self.isCorrectValue = isCorrectValue
        self.option = option

    #---------------------------------------------------------------#

    def getAnswerID(self):
        """
            Return the AnswerID
        """

        return self.answerID

    #---------------------------------------------------------------#

    def getContent(self):
        """
            Return the Answer Content
        """

        return self.content

    #---------------------------------------------------------------#

    def getAssessmentContent(self):
        """
            Return the content to be displayed when the answer is selected
        """

        return self.assessmentContent

    #---------------------------------------------------------------#

    def getReportContent(self):
        """
            Return the content to be displayed when the answer is selected
        """

        return self.reportContent

    #---------------------------------------------------------------#

    def hasReportContent(self):
        """ Does this answer have report content? """
        
        return self.getReportContent().strip() and True or False

    #---------------------------------------------------------------#

    def setReportContent(self, reportContent):
        """
            Return the content to be displayed when the answer is selected
        """

        self.reportContent = reportContent

    #---------------------------------------------------------------#

    def getScore(self):
        """
            Return the answer score when selected
        """

        return self.score

    #---------------------------------------------------------------#

    def isCorrect(self):
        """
            Return the answer is correct
        """

        return self.isCorrectValue
        
    #---------------------------------------------------------------#

    def setIsCorrect(self, isCorrect):
        """
            Set the answer as being correct
        """

        self.isCorrectValue = isCorrect

    #---------------------------------------------------------------#

    def getOption(self):
        """
            Returns the option text for the answer.  eg: A, B, C or ...            
        """

        return self.option

    #---------------------------------------------------------------#


#####################################################################
## MultipleChoiceInfo Class Definition
class MultipleChoiceInfo(ComponentInfo, ScorableInfo):
    """
        Defines the class info methods, defining information about
        the class without needing an instance of the class
    """

    #---------------------------------------------------------------#
    
    def getClassName(self):
        """
            Return the class name
        """
        
        return 'MultipleChoice'

    #---------------------------------------------------------------#

    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """
    
        return "Multiple Choice Question"
    
    #---------------------------------------------------------------#

    def getDescription(self):
        """
            Return a friendly description on the purpose
            of this class.  This description is often used in the 
            Property Editor for instances of this class
        """
    
        return "The Multiple Choice Question defines a single question with a set of multually exclusive Answers, that a user may select from during an Assessment."
        
    #---------------------------------------------------------------#

    def getIdentityType(self):
        """
            Return the identity type.  
        """
        
        return "mch"
        
    #---------------------------------------------------------------#
    
    def getClassIcon(self):
        """
            Return the appropriate icon file for the class.
        """
        
        return ImageFile('skins/MultipleChoice_icon.gif', globals())

    #---------------------------------------------------------------#

    def isInstanceOf(self, className):
        """
            Returns if the class is an instance of the specified
            className, or implements the interface specified 
            by the className
        """

        return className in ('Assessible', 'Exportable') or ScorableInfo.isInstanceOf(self,className) or ComponentInfo.isInstanceOf(self, className)
    
    #---------------------------------------------------------------#

    def onConfigureProperties(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific properties using the PropertyReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """

        #initialise the super-class properties for reflection
        ComponentInfo.onConfigureProperties(self)
        ScorableInfo.onConfigureProperties(self)
        
        #change the display name for 'content' to be 'Question'
        self.getPropertyDefinition("content").setDisplayName("Question")
        
        #change the description
        self.getPropertyDefinition("content").setDescription("The question to be asked (only appears when performing assessment)")

        #add the 'showCorrectness' property        
        self.addPropertyDefinition(PropertyDefinition(identity="showCorrectness", displayName="Display Correctness?", description="Display whether selected answers in assessments are correct/incorrect when generating reports for this component", defaultValue=false, propertyType=pt_BOOLEAN, propertyFormat=pf_SELECTION + pf_YES_NO, propertyStructure=ps_ATOMIC))        
        
        #add the 'answerList' property        
        self.addPropertyDefinition(PropertyDefinition(identity="answerList", displayName="Answers", description="The possible answers for the question", defaultValue=[Answer("A", "Answer A", "You selected answer A", "", 0, true, "A")], propertyType="ANSWER_LIST", propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC))        

    #---------------------------------------------------------------#

#####################################################################
## MultipleChoice Class Definition
class MultipleChoice(Component, Assessible, Scorable, KERLExtensions, Exportable):
    """
        Defines a simple component to represent one or more lines of 
        text entered during an assessment.
    """

    #---------------------------------------------------------------#

    #TRANSIENT: a ClassInfo implementation instance to provide info about this class
    _CLASSINFO = MultipleChoiceInfo()
    
    #---------------------------------------------------------------#

    # DTML Methods
    index_html = DTMLFile('dtml/index_html', globals())
    simplereport_html = DTMLFile('dtml/simplereport_html', globals())        
    scoredreport_html = DTMLFile('dtml/scoredreport_html', globals())
    scored360report_html = DTMLFile('dtml/scored360report_html', globals())
    groupedreport_html = DTMLFile('dtml/groupedreport_html', globals())    

    #---------------------------------------------------------------#

    def __init__(self, id, identity):
        "Constructor for the Component"        

        Component.__init__(self, id, identity)

    #---------------------------------------------------------------#

    def onAfterInstanciation(self):
        """
            Handle Knowledge Engine specific actions after this object 
            has been loaded into memory.  
            
            This is useful for performing backwards compatibility
        """
        
        #perform Component.onAfterInstanciation processing
        Component.onAfterInstanciation(self)

        #ensure that the optionList (if it exists) is a string (and not a list of strings)
        if hasattr(self, "optionList") and self.optionList is not None and type(self.optionList) is types.ListType:
            result = ""
            for s in self.optionList:
                result += s + "\n"
            self.optionList = result
            self._p_changed = 1

        #now upgrade the optionList to an answerList (if the optionList isn't already upgraded)
        if hasattr(self, "optionList") and self.optionList is not None:        
            #In the old days....
            # A valid option/answer must contain three items delimetered by '|'
            # the first must be any character - the ID
            # the second must be a number and can contain a decimal - the score
            # the third must be at least one character - the option
            answerList = []
            ids = [] #the id's used so far
            for option in self.optionList.splitlines(true): #true = keep eond of lines
                validoption = re.search(r'^\s*(.+)\s*\|\s*([0-9]?\.?[0-9]+)\s*\|\s*(.+)\s*$',option) # check for valid format
                if validoption:
                    id = validoption.group(1).strip()
                    score = float(validoption.group(2))
                    text = validoption.group(3).strip()
                    
                    #ensure the id won't cause a duplicate.. if it does, generate a new one
                    possibleids = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                    while ids.count(id) > 0 and len(possibleids) > 0: 
                        #get a new id (start at the end of the possible ids so we don't potentially overwrite the next option which we assume is using lower level letters)
                        id = possibleids[len(possibleids)-1]
                        possibleids = possibleids[0:len(possibleids)-2]

                    #ensure we've got a new id
                    if len(possibleids) > 0:
                        #remember the id
                        ids = ids + [id]
                        
                        #add the option as an answer (and set as the correct answer if it is the first)
                        #(set the default reportContent to be the text... so old reports will still work)
                        answerList = answerList + [Answer(id, text, "", id + ") " + text, score, len(answerList) == 0, id)]

                #set the answer
                self.setAnswerList(answerList)
                
                #TODO: remove the optionList attribute.. upgrade complete!
                self.optionList = None
                self._p_changed = 1

            ###print "DEBUG: MultipleChoice.onAfterInstanciation: Upgraded from optionList implementation to answerList implementation"

        #add the showCorrectness attribute if it doesn't exist.
        #(we set to false to maintain bakcwards compatibility)
        if not hasattr(self, "showCorrectness"):             
            self.showCorrectness = false
            self._p_changed = 1

        ###print "DEBUG: MultipleChoice.onAfterInstanciation: Completed"

    #---------------------------------------------------------------#

    def onAfterIdentityChange(self, identityMappings):
        """
            Handle Knowledge Engine specific actions after the 
            identity of this object has changed.
            
            The identityMappings dictionary holds 
            mappings from oldIdentities -> newIdentities
            (including this object)
                        
        """

        #perform default identity changes
        Component.onAfterIdentityChange(self, identityMappings)
        
        #resolve report KERFs for the answers
        for answer in self.getAnswerList():
            answer.setReportContent(updateKERF(answer.getReportContent(), identityMappings))
          
        #the answerlist has changed
        self._p_changed = 1
                
    #---------------------------------------------------------------#

    def getAnswerList(self):
        """
            Returns a list of Answer instances (in the order that they 
            were added
        """
            
        return self.answerList
                
    #---------------------------------------------------------------#

    def setAnswerList(self, answerList):
        """
            Sets a new list of Answer instances
        """
            
        self.answerList = answerList
        self._p_changed = 1

    #---------------------------------------------------------------#
        
    def getAnswer(self, answerID):
        """
            Return the answer with the specified id or None if
            not found
        """
        
        for answer in self.getAnswerList():        
            if answer.getAnswerID() == answerID:
                return answer        
        return None

    #---------------------------------------------------------------#
        
    def getScore(self, assessment):
        """
            Evalutes the report content to display

            RETURN TYPE: float        
        """
        
        #get the identity
        identity = self.getIdentity()

        #get the stored state
        id = assessment.getComponentState(identity).getValue()
        
        #get the answer
        answer = self.getAnswer(id)
        
        #is there an answer?
        if answer is not None:
            return answer.getScore()
        else:
            #we return 0 if there is no answer
            return 0

    #---------------------------------------------------------------#
        
    def getMaxScore(self):
        """
            Evaluates the maximum potential score
        """

        allscores = []
        for answer in self.getAnswerList():
            allscores = allscores + [answer.getScore()]
        return max(allscores)
        
    #---------------------------------------------------------------#

    def getShowCorrectness(self):
        """
            Return if correctness should be displayed in reports for this component
        """
        
        return self.showCorrectness
        
    #---------------------------------------------------------------#
        
    def isCorrect(self, assessment):
        """
            Returns if the specified answer for this component is correct

            RETURN TYPE: string        
        """
        
        #get the identity
        identity = self.getIdentity()

        #get the stored state
        id = assessment.getComponentState(identity).getValue()
        
        #determine if the answer is correct
        answer = self.getAnswer(id)        
        return answer is not None and answer.isCorrect()
        
    #---------------------------------------------------------------#
        
    def getAnswerReportContent(self, assessment):
        """
            Evalutes the answer report content to display

            RETURN TYPE: string        
        """
        
        #get the identity
        identity = self.getIdentity()

        #get the stored state
        id = assessment.getComponentState(identity).getValue()
        
        #get the answer
        answer = self.getAnswer(id)

        #is there an answer
        if answer is not None and answer.getReportContent() != "":
            return answer.getReportContent()                       
        else:
            return ""
        
    #---------------------------------------------------------------#

    def getComponentProperty(self, identity, propertyName, assessment = None):
        """
            Returns the value of the specified property, given the
            identity of the component (which may be used to lookup states) and
            an assessment.

            RETURNS: None if the property doesn't exist
        """

        if propertyName == "value":
            return assessment.getComponentState(identity).getValue()
        else:
            return Component.getComponentProperty(self, identity, propertyName, assessment)
        
        
    #---------------------------------------------------------------#
    # BEGIN: Assessible Implementation                              #
    #---------------------------------------------------------------#
        
    def getAssessmentValueType(self):
        """
            ABSTRACT: Returns the type of the value that is to be stored
            for this component when it's assessment state is persisted.
            
            NOTE 1: See AssessmentComponentValueTypes for a list of defined contants       
            NOTE 2: This method MUST be overriden in the implementing
            class to specify the correct type for value persistence
            
            RETURN TYPE: String
        """
        
        #multiplechoice assessment values are strings
        return st_STRING
        
    #---------------------------------------------------------------#
        
    def getDefaultAssessmentState(self, assessment):
        """
            ABSTRACT: Constructs and returns a suitable 'default' 
            AssessmentComponentState for the provided Assessment
            instance.

            NOTE: This method MUST be overriden in the implementing
            class to provide a suitable default value for the component
            
            RETURN TYPE: AssessmentComponentState
        """
        
        return AssessmentComponentState(assessment.getIdentity(), assessment.getModelIdentity(), self.getIdentity(), self.getAssessmentValueType(), "", revision=assessment.getRevision())
            
    #---------------------------------------------------------------#

    def updateAssessmentState(self, assessment, formValue):
        """
            ABSTRACT: Updates the specified assessment using
            the provided HTTP REQUEST formValue.
            
            NOTE: This method MUST be overriden in the implementing
            class to suitably update the component state
        """
        
        Component.updateAssessmentState( self, assessment, formValue )            
            
        #get the current assessment state for the component from the assessment
        state = assessment.getComponentState(self.getIdentity())
        
        #update the current state
        state.setValue(formValue)
        
        #update the assessment
        assessment.setIsModified(true)
            
        searchIndex = getattr(self, 'searchIndex', False)
        if searchIndex:
            state.setSearchIndex(searchIndex)
            
            chosen = self.getAnswer(formValue)
            if chosen:
                state.setSearchValue(chosen.getContent())
            else:
                state.setSearchValue(None)                
        
            
    #---------------------------------------------------------------#
    # END: Assessible Implementation                                #
    #---------------------------------------------------------------#


    #---------------------------------------------------------------#
    # BEGIN: Exportable Implementation                              #
    #---------------------------------------------------------------#
    def getComponentCSVValue(self, assessment, displayReportContent=False):
        """
            Return the CSV friendly version of the component.
        """

        if displayReportContent:
            return self.getAnswerReportContent(assessment)
        else:
            return self.getComponentValue(assessment)
    #---------------------------------------------------------------#
    # END: Exportable Implementation                                #
    #---------------------------------------------------------------#

    
    #---------------------------------------------------------------#
    # BEGIN: KERLExtensions Implementation                          #
    #---------------------------------------------------------------#

    def compileKERLtoJavaScript(self, expression):
        """
            Returns a Python string that represents a JavaScript
            expression, which may be used to evaluate the status
            of the component class specific KERL expression.

            NOTE: this method is primarily used to produce
            JavaScript expressions that form the basis of the
            client-side showif technology.
            
            RETURN TYPE: String         
        """
        
        
        #example: cond="['mcq1','selected', 'id']" returns 'document.getElementById("mcq1").value==value'

        #get the identity (which is the identity of itself)        
        identity = self.getIdentity()

        #determine the operator we are going to process
        opr = expression[1]
        #-------------------------------
        #an answer is 'selected' if the actual selected answer is the specified answer
        if opr == "selected":

            return "document.getElementById(\"%s\").value == \"%s\"" % (identity, asJavaScript(expression[2]))
            
        #-------------------------------
        #an answer is 'unselected' if the actual selected answer isn't the specified answer
        elif opr == "unselected":

            return "document.getElementById(\"%s\").value != \"%s\"" % (identity, asJavaScript(expression[2]))
            
        #-------------------------------
        #a question is correct if the selected answer (there must be one) is correct
        elif opr == "correct":

            #find the correct answer
            for answer in self.getAnswerList():
                
                #remember the correct answer
                if answer.isCorrect():
                    correctAnswer = answer

            #now ensure that the selected answer is the correct answer
            return "document.getElementById(\"%s\").value == \"%s\"" % (identity, asJavaScript(correctAnswer.getAnswerID()))
            
           
        #-------------------------------
        #a question is incorrect if the selected answer isn't the correct one
        elif opr == "incorrect":

            #find the correct answer
            for answer in self.getAnswerList():
                
                #remember the correct answer
                if answer.isCorrect():
                    correctAnswer = answer

            #ensure that the selected answer is not the correct answer
            return "document.getElementById(\"%s\").value != \"%s\"" % (identity, asJavaScript(correctAnswer.getAnswerID()))
        
        #-------------------------------
        #a question has been 'attempted' iff one of the answers are selected
        elif opr == "attempted":
        
            javascript = ""
            for answer in self.getAnswerList():
            
                #add an || or operator between expressions
                if javascript != "":
                    javascript += " || "
                
                #add the condition for the current answer
                javascript += "document.getElementById(\"%s\").value == \"%s\"" % (identity, asJavaScript(answer.getAnswerID()))
                
            return "(" + javascript + ")"
        
        #-------------------------------
        #a question is 'notattempted' iff none of the answers are selected
        elif opr == "notattempted":

            javascript = ""
            for answer in self.getAnswerList():
            
                #add an && and operator between expressions
                if javascript != "":
                    javascript += " && "
                
                #add the condition for the current answer
                javascript += "document.getElementById(\"%s\").value != \"%s\"" % (identity, asJavaScript(answer.getAnswerID()))
                
            return "(" + javascript + ")"
        
        #-------------------------------
        else:
            raise InvalidKerlOperator, opr
            #return "true"

    #---------------------------------------------------------------#

    def compileKERLtoEnglish(self, expression):
        """
            Returns a string representing an 'English' version
            of the component class specific KERL expression.

            NOTE: this method is primarily used to produce
            English descriptions of KERL expressions that may be 
            used to describe things like AssessmentFilters
            
            RETURN TYPE: String         
        """
        
        #determine the operator we are going to process
        operator = expression[1]
        
        #-------------------------------
        #an answer is 'selected' if the actual selected answer is the specified answer
        if operator == "selected":

            return "%s '%s' answer %s is selected" % (self.getClassInfo().getDisplayName(), self.getTitleOrId(), self.getAnswer(expression[2]).getOption())
            
        #-------------------------------
        #an answer is 'unselected' if the actual selected answer isn't the specified answer
        elif operator == "unselected":

            return "%s '%s' answer %s is not selected" % (self.getClassInfo().getDisplayName(), self.getTitleOrId(), self.getAnswer(expression[2]).getOption())
            
        #-------------------------------
        #a question is correct if the selected answer (there must be one) is correct
        elif operator == "correct":

            return "%s '%s' correctis answer selected" % (self.getClassInfo().getDisplayName(), self.getTitleOrId())
            
           
        #-------------------------------
        #a question is incorrect if the selected answer isn't the correct one
        elif operator == "incorrect":

            return "%s '%s' incorrect is answer selected" % (self.getClassInfo().getDisplayName(), self.getTitleOrId())
        
        #-------------------------------
        #a question has been 'attempted' iff one of the answers are selected
        elif operator == "attempted":
        
            return "%s '%s' is attempted" % (self.getClassInfo().getDisplayName(), self.getTitleOrId())
        
        #-------------------------------
        #a question is 'notattempted' iff none of the answers are selected
        elif operator == "notattempted":

            return "%s '%s' is not attempted" % (self.getClassInfo().getDisplayName(), self.getTitleOrId())
        
        #-------------------------------
        else:
            raise InvalidKerlOperator, operator
            #return ""

    #---------------------------------------------------------------#

    def evaluate(self, expression, assessment):
        """
            Evalutes the component class specific KERL expression
            using the provided assessment.

            NOTE: this method is primarily used to produce
            JavaScript expressions that form the basis of the
            client-side showif technology.

            RETURN TYPE: Boolean         
        """
        
        #get the identity (which is the identity of itself)        
        identity = self.getIdentity()

        #get the stored state
        state = assessment.getComponentState(identity)

        #determine the operator we are going to process
        opr = expression[1]
        #-------------------------------
        #an answer is 'selected' if the actual selected answer is the specified answer
        if opr == "selected":

            return state.getValue() == expression[2]
            
        #-------------------------------
        #an answer is 'unselected' if the actual selected answer isn't the specified answer
        elif opr == "unselected":

            return state.getValue() != expression[2]
            
        #-------------------------------
        #a question is correct if the selected answer (there must be one) is correct
        elif opr == "correct":

            #find the correct answer
            for answer in self.getAnswerList():
                
                #remember the correct answer
                if answer.isCorrect():
                    correctAnswer = answer

            #now ensure that the selected answer is the correct answer
            return state.getValue() == correctAnswer.getAnswerID()
            
           
        #-------------------------------
        #a question is incorrect if the selected answer isn't the correct one
        elif opr == "incorrect":

            #find the correct answer
            for answer in self.getAnswerList():
                
                #remember the correct answer
                if answer.isCorrect():
                    correctAnswer = answer

            #ensure that the selected answer is not the correct answer
            return state.getValue() != correctAnswer.getAnswerID()
        
        #-------------------------------
        #a question has been 'attempted' iff one of the answers are selected
        elif opr == "attempted":
        
            attempted = false
            for answer in self.getAnswerList():            
                attempted = attempted or (state.getValue() == answer.getAnswerID())
                
            return attempted
        
        #-------------------------------
        #a question is 'notattempted' iff none of the answers are selected
        elif opr == "notattempted":

            notattempted = true
            for answer in self.getAnswerList():            
                notattempted = notattempted and (state.getValue() != answer.getAnswerID())
                
            return notattempted
        
        #-------------------------------
        else:
            raise InvalidKerlOperator, opr
            #return true
    
    #---------------------------------------------------------------#

    def getDependencies(self, expression):
        """
            Returns a list of components that are required to
            evaluate the component class specific KERL expression.

            NOTE: Includes the specific component itself.

            RETURN TYPE: {Component} indexed by Component.getIdentity()
       """

        return {self.getIdentity():self}
    
    #---------------------------------------------------------------#

    def updateKERLIdentities(self, expression, identityMappings):
        """
            Returns the expression with the appropriate old identities
            (as identified by the identityMappings) replaced with 
            new identities.
        """
        
        #get the component identity from the expression (first element)
        identity = expression[0]
        
        #is the component in the identityMappings?
        if identityMappings.has_key(identity):
            expression[0] = identityMappings[identity]
        
        return expression

    #---------------------------------------------------------------#
    # END: KERLExtensions Implementation                            #
    #---------------------------------------------------------------#

    def generateGraphableStatistics(self, baseAssessmentFilter = None, user = None, nrAssessments = None):
        """
            Generates a Graphable instance containing statistic
            information for this component - that may be graphed.
            
            The provided assessmentFilter is used as a basis for
            generating statistics information.  Setting this to
            None (the default) means all assessments are used.
            
            RETURN TYPE: Graphable
        """
        
        #ensure the baseAssessmentFilter is valid (not None)
        if baseAssessmentFilter is None:
            baseAssessmentFilter = self.getModel().getAllAssessmentsFilter()
        
        #we need to use the assessment storage service to get the statistics
        assessmentStorageService = self.getModel().getAssessmentStorageService()

        #determine the number of assessments (if necessary)
        if nrAssessments is None:
            nrAssessments = assessmentStorageService.getNrAssessmentsSatisfyingFilter(baseAssessmentFilter, user)   
        
        #we need a Graphable for the results
        graphable = Graphable(self.getTitleOrId(), "The following chart outlines the selection statistics for the '%s' component in the %d current assessments." % (self.getTitleOrId(), nrAssessments), 0, nrAssessments)
        
        #create a datum for the number of times each answer has been selected in the assessments
        for answer in self.getAnswerList():

            #create a filter for the answer      
            filterTitle = "# Answer %s)." % answer.getOption()
            filterExpression = parseKERL("['%s', 'selected', '%s']" % (self.getIdentity(), answer.getAnswerID()))
            assessmentFilter = AssessmentFilter(title=filterTitle, expression=simplifyKERL(['all', baseAssessmentFilter.getExpression(), filterExpression]), model=self.getModel(), assessmentScope=baseAssessmentFilter.getAssessmentScope())
            
            #determine the color for the datum
            if self.getShowCorrectness():
                if answer.isCorrect():
                    datumColor = "green"
                    datumCallout = "Correct"
                else:
                    datumColor = "red"
                    datumCallout = "Incorrect"
            else:
                datumColor = "green"
                datumCallout = None

            #filter the assessments
            nrSelectedAssessments = assessmentStorageService.getNrAssessmentsSatisfyingFilter(assessmentFilter, user)
            datum = Datum(identity=answer.getAnswerID(), title="# Answer %s)." % answer.getOption(), value=nrSelectedAssessments, color=datumColor, footnote=answer.getContent(), callout = datumCallout, context={"ASSESSMENT_FILTER":assessmentFilter})
            graphable.addDatum(datum)
            
            #remove the actual selections to work out the undefined count
            nrAssessments = nrAssessments - nrSelectedAssessments

        undefinedKerl = [self.getIdentity(), 'notattempted']

        undefinedFilter = AssessmentFilter(title="Undefined Assessments", expression=simplifyKERL(['all', baseAssessmentFilter.getExpression(), undefinedKerl]), 
                                           model=self.getModel(), assessmentScope=baseAssessmentFilter.getAssessmentScope())

        #create a datum for the number of times no choices have been made in the assessments
        datum = Datum(identity="undefined", title="# Undefined.", value=nrAssessments, color="orange", callout = "Incorrect", context = {'ASSESSMENT_FILTER': undefinedFilter},
                      footnote="As it is possible for an end-user to avoid selecting an answer, some assessments may have 'undefined' answers.")        
        graphable.addDatum(datum)
        
        return graphable

    #---------------------------------------------------------------#

    def listActions(self, info=None):
        """
            PLONE Support:

            Returns a tuple of ActionInformation instances
            specifying the available CMF Actions for the object

            overrides:

            (CMF Actions are different from Knowledge Engine Actions)
        """

        #add the super-class actions first
        lCMFActions = Component.listActions(self, info)

        #add the 'statistics' action
        lCMFActions = lCMFActions + (
                        ActionInformation(
                            id='statistics',
                            title='statistics',
                            description='Current Statistics',
                            category='object',
                            condition='',
                            permissions=(permissions.ListFolderContents,),
                            priority=1,
                            visible=1,
                            action='string:${object_url}/MultipleChoice_Statistics'
                        ),)
                        
        return lCMFActions

    #---------------------------------------------------------------#

    def hasReportContent(self):
        """ Does this component have report content? """
        
        # The default report content fields are set, so yes.
        if Component.hasReportContent(self):
            return True

              
        # Check each answer for report content              
        for answer in self.getAnswerList():
            if answer.hasReportContent():
                return True
            
        return False


#####################################################################
## Class Initialisation

# Register Component Information with the Knowledge Engine
registerGenericObjectClass(MultipleChoice)

InitializeClass(Answer)
#####################################################################
